package qj.tool.db.orientdb;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import qj.tool.db.ElementType;
import qj.tool.db.Key;
import qj.tool.vi.ViMark;
import qj.util.ReflectUtil;
import qj.util.funct.F1;
import qj.util.funct.Fs;
import qj.util.funct.P2;

import com.orientechnologies.orient.core.record.impl.ODocument;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class Analyzer {

	public static <A> ClassConverter<A> analyze(final A a, F1<Class,ClassConverter> ccF) {
		Class class1 = a.getClass();
		return (ClassConverter<A>) analyze(class1, ccF);
	}

	public static <A> ClassConverter<A> analyze(final Class<A> clazz, F1<Class,ClassConverter> ccF) {
		
		final HashMap<String,Field> fields = new HashMap<String,Field>();
		final List<P2<Object, ODocument>> oteTasks = new LinkedList<P2<Object, ODocument>>();
		final List<P2<ODocument,Object>> etoTasks = new LinkedList<P2<ODocument,Object>>();
		
		analyze(clazz, fields, oteTasks, etoTasks, ccF);

		final Field idField = fields.get("id");
		
		return new ClassConverter<A>() {
			public P2<Object, ODocument> ote() {
				return new P2<Object, ODocument>() {public void e(Object o, ODocument e) {
					for (P2<Object,ODocument> p2 : oteTasks) {
						p2.e(o, e);
					}
				}};
			}
			public A eto(ODocument e) {
				A o = ReflectUtil.newInstance(clazz);

				for (P2<ODocument,Object> pe : etoTasks) {
					pe.e(e, o);
				}
				return o;
			}

			public KeyImpl getKey(A obj) {
				return (KeyImpl) getFieldValue(idField, obj);
			}
			public HashMap<String,Field> fields() {
				return fields;
			}
			@Override
			public void setKey(Key key, A obj) {
				ReflectUtil.setFieldValue(key, idField, obj);
			}
		};
	}
	

	public static void analyze(Class<?> clazz, HashMap<String, Field> fields,
			List<P2<Object, ODocument>> oteTasks,
			List<P2<ODocument,Object>> etoTasks, F1<Class, ClassConverter> ccF) {
		for (final Field field : clazz.getDeclaredFields()) {
			int modifiers = field.getModifiers();
			if ((modifiers & (Modifier.STATIC | Modifier.FINAL | Modifier.TRANSIENT)) > 0
					|| (modifiers & Modifier.PUBLIC) == 0
					) {
				continue;
			}
			
			String name = field.getName();
	
			if (fields.containsKey(name)) {
				continue;
			}
			fields.put(name,field);
	
			oteTasks.add(ote(field, ccF));
			
			final F1<ODocument, Object> eto = eto(field, ccF);
			if (eto != null) {
				
				etoTasks.add(new P2<ODocument,Object>() {public void e(ODocument doc, Object c) {
					Object value = eto.e(doc);
					if (value != null) {
						ReflectUtil.setFieldValue(value, field, c);
					}
				}});
			}
		}
		
		if (clazz.getSuperclass() != Object.class) {
			analyze(clazz.getSuperclass(), fields, oteTasks, etoTasks, ccF);
		}
	}

	public static F1<ODocument, Object> eto(final Field field, F1<Class, ClassConverter> ccF) {
		final String name = field.getName();
		final Class<?> type = field.getType();
		
		if (name.equals("id")) {
			return new F1<ODocument, Object>() {public Object e(ODocument doc) {
				return new KeyImpl(doc.getIdentity());
			}};
		}
		
		final F1<Object,Object> etoValueF;
		
		if (type.equals(String.class)) {
			etoValueF = new F1<Object,Object>() {public Object e(Object value) {
				return ViMark.normalizeSpecialUnicodeEncoding((String) value);
			}};
		} else if (type.equals(byte[].class)) {
			etoValueF = Fs.f1();
		} else if (Key.class.isAssignableFrom(type)) {
			etoValueF = new F1<Object,Object>() {public Object e(Object value) {
				return KeyImpl.fromString((String)value);
			}};
		} else if (Date.class.isAssignableFrom(type)) {
			etoValueF = new F1<Object,Object>() {public Object e(Object value) {
				return new Date((Long)value);
			}};
		} else if (type.equals(Integer.class) || type.equals(int.class)) {
			etoValueF = new F1<Object,Object>() {public Object e(Object value) {
				if (value instanceof Long) {
					return ((Long)value).intValue();
				}
				return value;
			}};
		} else if (type.equals(BigDecimal.class)) {
			etoValueF = new F1<Object,Object>() {public Object e(Object value) {
				if (value instanceof Long) {
					return new BigDecimal((Long)value);
				}
				return value;
			}};
		} else if (List.class.isAssignableFrom(type) || Set.class.isAssignableFrom(type)) {
			final ElementType eleTypeAno = field.getAnnotation(ElementType.class);
			if (eleTypeAno != null) {
				Class eleType = eleTypeAno.value();
				if (eleType.equals(Key.class)) {
					etoValueF = List.class.isAssignableFrom(type) ?
							new F1<Object,Object>() {public Object e(Object value) {
								return deserializeKeyList((List) value);
							}} : 
							new F1<Object,Object>() {public Object e(Object value) {
								return deserializeKeySet((Set) value);
							}};
				} else {
					final ClassConverter cc = ccF.e(eleType);

					etoValueF = List.class.isAssignableFrom(type) ?
							new F1<Object,Object>() {public Object e(Object value) {
								List<ODocument> embededList = (List<ODocument>)value;
								LinkedList ret = new LinkedList();
								
								for (ODocument embededDoc : embededList) {
									ret.add(cc.eto(embededDoc));
								}
								
								return ret;
							}} :
							new F1<Object,Object>() {public Object e(Object value) {
								Set<ODocument> embededList = (Set<ODocument>)value;
								HashSet ret = new HashSet();
								
								for (ODocument embededDoc : embededList) {
									ret.add(cc.eto(embededDoc));
								}
								
								return ret;
							}}
					;
				}
			} else {
				etoValueF = Fs.f1();
			}
		} else {
			etoValueF = Fs.f1();
		}
		return new F1<ODocument, Object>() {public Object e(ODocument doc) {
			
			Object value = doc.field(name);
			if (value == null) {
				return null;
			}
			return etoValueF.e(value);
		}};
	}

	public static P2<Object, ODocument> ote(final Field field, F1<Class, ClassConverter> ccF) {
		final String name = field.getName();
		Class<?> fieldType = field.getType();
		
		final F1<Object,Object> oteValueF;
		if (Key.class.isAssignableFrom(fieldType)) {

			oteValueF = new F1<Object,Object>() {public Object e(Object value) {
				return KeyImpl.toString((KeyImpl)value);
			}};
		} else if (String.class.isAssignableFrom(fieldType)) {
			oteValueF = new F1<Object,Object>() {public Object e(Object value) {
				return ViMark.normalizeSpecialUnicodeEncoding((String) value);
			}};
		} else if (Date.class.isAssignableFrom(fieldType)) {
			oteValueF = new F1<Object,Object>() {public Object e(Object value) {
				Date date = (Date) value;
				return date.getTime();
			}};
		} else if (List.class.isAssignableFrom(fieldType) || Set.class.isAssignableFrom(fieldType)) {
			ElementType eleTypeAnno = field.getAnnotation(ElementType.class);
			if (eleTypeAnno != null) {
				Class eleType = eleTypeAnno.value();
				if (eleType.equals(Key.class)) {
					oteValueF = List.class.isAssignableFrom(fieldType) ? 
							new F1<Object,Object>() {public Object e(Object value) {
								return serializeKeyList((List) value);
							}} :
							new F1<Object,Object>() {public Object e(Object value) {
								return serializeKeySet((Set) value);
							}};
				} else {
					final ClassConverter cc = ccF.e(eleType);
					oteValueF = List.class.isAssignableFrom(fieldType) ?
							new F1<Object,Object>() {public Object e(Object value) {
								List list = (List) value;
								LinkedList<ODocument> embeddedList = new LinkedList<ODocument>();
								for (Object eleO : list) {
									ODocument embededDoc = new ODocument();
									cc.ote().e(eleO, embededDoc);
									embeddedList.add( embededDoc );
								}
								return embeddedList;
							}} : 
							new F1<Object,Object>() {public Object e(Object value) {
								Set set = (Set) value;
								HashSet<ODocument> embeddedSet = new HashSet<ODocument>();
								for (Object eleO : set) {
									ODocument embededDoc = new ODocument();
									cc.ote().e(eleO, embededDoc);
									embeddedSet.add( embededDoc );
								}
								return embeddedSet;
							}};
				}
			} else {
				oteValueF = Fs.f1();
			}
		} else {
			oteValueF = Fs.f1();
		}
		
		P2<Object, ODocument> ote = new P2<Object, ODocument>() {public void e(Object o, final ODocument e) {
			Object value = getFieldValue(field, o);
			
			if (value == null) {
				e.removeField(name);
			} else {
				e.field(name, oteValueF.e(value));
			}
		}};
		return ote;
	}	
	

	protected static Object serializeKeySet(Set<KeyImpl> setOri) {
		if (setOri == null) {
			return null;
		}
		HashSet<String> ret = new HashSet<String>();
		for (KeyImpl keyImpl : setOri) {
			ret.add(KeyImpl.toString(keyImpl));
		}
		return ret;
	}
	protected static Set<Key> deserializeKeySet(Set<String> keySet) {
		if (keySet==null) {
			return null;
		}
		
		HashSet<Key> ret = new HashSet<Key>();
		
		for (String keyStr : keySet) {
			ret.add(KeyImpl.fromString(keyStr));
		}
		
		return ret;
	}
	
	
	protected static Object serializeKeyList(List<KeyImpl> listOri) {
		if (listOri == null) {
			return null;
		}
		LinkedList<String> ret = new LinkedList<String>();
		for (KeyImpl keyImpl : listOri) {
			ret.add(KeyImpl.toString(keyImpl));
		}
		return ret;
	}
	protected static List<Key> deserializeKeyList(List<String> keyList) {
		if (keyList==null) {
			return null;
		}
		
		LinkedList<Key> ret = new LinkedList<Key>();
		
		for (String keyStr : keyList) {
			ret.add(KeyImpl.fromString(keyStr));
		}
		
		return ret;
	}

	public static Object getFieldValue(final Field field, Object obj) {
		if (obj==null) {
			return null;
		}
		return ReflectUtil.getFieldValue(field, obj);
	}
}
